/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved.
 *
 */

import path from 'path';

import { hvigorCore, MetricLogType, Project , FileSet , TaskInputValue } from '@ohos/hvigor';
import zip from '@zip.js/zip.js';
import fse from 'fs-extra';

import { PackingToolOptions } from '../builder/inner-java-command-builder/packing-tool-options.js';
import { ModulePathInfoIml } from '../common/iml/module-path-info-iml.js';
import { BuildArtifactConst, BuildArtifactExtension, BuildDirConst } from '../const/build-directory-const.js';
import { BuildModeConst } from '../const/build-mode-const.js';
import { CommonConst, HvigorConfigConst } from '../const/common-const.js';
import { ProjectModel } from '../model/project/project-model.js';
import { ProjectBuildProfile } from '../options/build/project-build-profile.js';
import { FileUtil } from '../utils/file-util.js';
import { ProcessUtils } from '../utils/process-utils.js';
import { isFastBuildApp } from '../utils/task-util.js';
import { BasePackAppTask } from './base/base-pack-app-task.js';
import { TaskNames } from './common/task-names.js';
import { ModuleTargetData } from './data/hap-task-target-data.js';
import { GeneratePackRes } from './generate-pack-res.js';
import { ProjectTaskService } from './service/project-task-service.js';

import Task = TaskNames.Task;
import ProductBuildOpt = ProjectBuildProfile.ProductBuildOpt;

import { ModuleTaskService } from './service/module-task-service.js';

import RemoteHspOpt = ProjectBuildProfile.RemoteHspOpt;

import { HvigorConfigLoader } from '@ohos/hvigor/src/common/util/hvigor-config-loader.js';

import { initSignRemoteHspMap } from '../utils/remote-hsp-utils.js';

import process from 'process';
import os from 'os';

import { DependencyManager } from '../project/dependency/dependency-manager.js';

/**
 * 调用打包工具生成.app
 *
 * @since 2022/1/10
 */
export class PackageApp extends BasePackAppTask {
  private readonly allDestDir: string[] = [];
  private readonly allSrcHapPath: string[] = [];
  private readonly allDestHapPath: string[] = [];
  private readonly allSrcHspPath: string[] = [];
  private allDestHspPathList: string[] = [];
  private readonly allRemoteHspPathMap: Map<string, RemoteHspOpt> = new Map<string, RemoteHspOpt>();

  private readonly remoteHspModuleSet: Set<string> = new Set<string>();
  private readonly projectBuildProfilePath: string = '';
  private readonly projectModel: ProjectModel;
  private readonly curProduct: ProductBuildOpt;
  private readonly defaultPackageLimitSize: number = 2;
  private readonly pacJsonFilePath: string;

  declareExecutionCommand(): string {
    return `${this.getPackAppCommand().toString()}`;
  }

  declareInputs(): Map<string, TaskInputValue> {
    return new Map<string, TaskInputValue>().set('allDestDir', this.allDestDir);
  }

  declareInputFiles(): FileSet {
    const fileSet = new FileSet().addEntries([this.packInfoPath, ...this.allSrcHapPath, ...this.allSrcHspPath], {
      isDirectory: false,
    });
    if (this.needPackRes) {
      fileSet.addEntry(this.packResPath, { isDirectory: true });
    }
    if (fse.existsSync(this.pacJsonFilePath)) {
      fileSet.addEntry(this.pacJsonFilePath);
    }
    return fileSet;
  }

  declareOutputFiles(): FileSet {
    return super
      .declareOutputFiles()
      .addEntries(
        [
          path.resolve(this.service.getPathInfo().getProjectOutputPath(), this.service.getAppOutputFileName()),
          ...this.allDestHapPath,
          ...this.allDestHspPathList,
        ],
        { isDirectory: false },
      )
      .addEntry(this.getDestinationPath(), { isDirectory: true }); // app产物debug symbol加入增量输出检测
  }

  constructor(project: Project, taskService: ProjectTaskService) {
    super(project, taskService, Task.PACKAGE_APP);
    this.projectBuildProfilePath = taskService.getProjectModel().getProfilePath();
    this.curProduct = taskService.getTargetProduct();
    this.projectModel = taskService.getProjectModel();
    this.pacJsonFilePath = path.resolve(this.service.getPathInfo().getProjectOutputPath(), CommonConst.PAC_ONLY_JSON);
  }

  initTaskDepends() {
    this.dependsOn(GeneratePackRes.name);
  }

  async beforeAlwaysAction() {
    const projectRemoteHspPath = this.service.getProjectModel().getRemoteHspPath();
    this.service.getProductDataMap().forEach((value) => {
      for (const moduleTargetData of value) {
        const modulePathInfo: ModulePathInfoIml = moduleTargetData.getPathInfo();
        const moduleBuildOutputPath: string = modulePathInfo.getModuleBuildOutputPath();
        const isFastBuild = isFastBuildApp(this.service);

        if (moduleTargetData.getModuleModel().isHapModule()) {
          isFastBuild ?
            this.fastInitWithHapModuleData(moduleTargetData, moduleBuildOutputPath) :
            this.initWithHapModuleData(moduleTargetData, moduleBuildOutputPath);
        } else {
          isFastBuild ?
            this.fastInitWithHspModuleData(moduleTargetData, moduleBuildOutputPath) :
            this.initWithHspModuleData(moduleTargetData, moduleBuildOutputPath);
        }
        const moduleModel = moduleTargetData.getModuleModel();
        const dependencyManager = new DependencyManager(this.service.getProjectModel().isFaMode(),
          moduleModel, this.service.getProjectModel());
        const { npmDependencies } = dependencyManager.collectAllDependencies();
        const moduleRemoteHspPath = moduleModel.getRemoteHspPath();
        if (!this.remoteHspModuleSet.has(moduleModel.getName())) {
          initSignRemoteHspMap(moduleRemoteHspPath, this.allRemoteHspPathMap, npmDependencies);
          initSignRemoteHspMap(projectRemoteHspPath, this.allRemoteHspPathMap, npmDependencies);
          this.remoteHspModuleSet.add(moduleModel.getName());
        }
      }
    });

    // allDestHspPathList新增remoteHsp路径，用于打包app
    // 修改该方法调用位置，由于getPackAppCommand涉及增量判断调用两次所以constructor进行处理
    const allOhModulesHspPathList = [...this.allRemoteHspPathMap.values()];
    allOhModulesHspPathList.forEach((remoteHspOpt) => {
      const integratedRemoteHspPath = this.projectModel.getCacheIntegratedHspPath(this.curProduct.name);
      const inputPath = remoteHspOpt.isIntegratedHsp
        ? path.resolve(integratedRemoteHspPath, remoteHspOpt.hspDirName, remoteHspOpt.hspFileName)
        : remoteHspOpt.hspPath;
      this.allDestHspPathList.push(inputPath);
    });
  }

  protected async doTaskAction(): Promise<void> {
    const subTaskName = 'generate app packaging command';
    const subDurationEvent = this.durationEvent.createSubEvent(subTaskName, '');
    subDurationEvent.start();

    await this.getUnpackedHaps();
    const command: string[] = this.getPackAppCommand();
    this._log._printDebugCommand('PackageApp', command);

    subDurationEvent.stop();
    subDurationEvent.setLog(subTaskName, MetricLogType.INFO);
    const executeTaskName = 'execute app packaging command';
    const subToolExecuteEvent = this.durationEvent.createSubEvent(executeTaskName, '');
    subToolExecuteEvent.start();

    await new ProcessUtils().execute(command);
    subToolExecuteEvent.stop();
    subToolExecuteEvent.setLog(executeTaskName, MetricLogType.INFO);

    // 增加收集debug symbols的业务逻辑
    if (this.shouldCollectProjectDebugSymbol()) {
      this.collectDebugSymbolInAppScope();
      // 将目标文件夹打包成zip
      await this.buildZipPackage();
    }
  }

  /**
   * 获得打包app的命令
   * @returns {string[]} 打包app的命令
   */
  private getPackAppCommand(): string[] {
    const packageOptions = new PackingToolOptions();
    packageOptions.addCalledJarFile(this.packageTool);
    if (this.needPackRes && fse.existsSync(this.packResPath)) {
      packageOptions.addPackResPath(this.packResPath);
    }
    const mode = isFastBuildApp(this.service) ? 'fastApp' : 'app';
    packageOptions
      .addMode(mode)
      .addPackInfoPath(this.packInfoPath)
      .addHapPath(this.allDestHapPath.join(','))
      .addHspPath(this.allDestHspPathList.join(','))
      .force(true)
      .addOutPath(path.resolve(this.service.getPathInfo().getProjectOutputPath(), this.service.getAppOutputFileName()))
      .addMainModuleLimit(this.defaultPackageLimitSize)
      .addNormalModuleLimit(this.defaultPackageLimitSize);

    // 判断是否要添加压缩参数
    const pressLevel = HvigorConfigLoader.getInstance().getPropertiesConfigValue<string>(HvigorConfigConst.OHOS_PACK_COMPRESS_LEVEL);
    if (pressLevel) {
      packageOptions.addCompressLevel(pressLevel, this.service.getSdkInfo().getSdkVersion());
    }
    if (fse.existsSync(this.pacJsonFilePath)) {
      packageOptions.addPacArg(this.pacJsonFilePath);
    }
    return packageOptions.build();
  }

  /**
   * 遍历product的所有hap包,
   * 将需要打到app的hap去除-unsigned, 然后生成到同级的app目录下, 最后输出路径到字符串数组中
   */
  private async getUnpackedHaps(): Promise<void> {
    for (const destDir of this.allDestDir) {
      FileUtil.checkDirWithoutDelete(destDir);
    }
    const copyHaps = [];
    const copyHsps = [];
    this.checkOutputName(this.allSrcHapPath);
    for (let i = 0; i < this.allSrcHapPath.length; i++) {
      copyHaps.push(fse.copy(this.allSrcHapPath[i], this.allDestHapPath[i]));
    }
    this.checkOutputName(this.allSrcHspPath);
    for (let i = 0; i < this.allSrcHspPath.length; i++) {
      copyHsps.push(fse.copy(this.allSrcHspPath[i], this.allDestHspPathList[i]));
    }
    await Promise.all([...copyHaps, ...copyHsps]);
  }

  private initWithHapModuleData(moduleTargetData: ModuleTargetData, moduleBuildOutputPath: string) {
    const outputHapFileName: string = moduleTargetData.getModuleTargetOutputFileName('', false);
    const destDir = path.resolve(moduleBuildOutputPath, 'app');
    this.allDestDir.push(destDir);
    let relatedEntryModules = moduleTargetData.getRelatedEntryModules();
    const moduleType = moduleTargetData.getModuleModel().getModuleType();
    if (ModuleTaskService.checkEntryModules(moduleType, relatedEntryModules)) {
      if (!this.service.isFaMode()) {
        relatedEntryModules = [relatedEntryModules![0]];
      }
      for (const entryName of relatedEntryModules!) {
        const outputHapFileNameWithEntry = moduleTargetData.getModuleTargetOutputFileName(entryName, false);
        const hapPathWithEntry: string = path.resolve(moduleBuildOutputPath, outputHapFileNameWithEntry);
        const destFile = this.destFileResolve(destDir, outputHapFileNameWithEntry);
        this.allSrcHapPath.push(hapPathWithEntry);
        this.allDestHapPath.push(destFile);
      }
    } else {
      if (moduleTargetData.isSingleDeviceTypeTarget()) {
        const hapPath: string = path.resolve(moduleBuildOutputPath, outputHapFileName);
        const destFile = this.destFileResolve(destDir, outputHapFileName);
        this.allDestDir.push(destDir);
        this.allSrcHapPath.push(hapPath);
        this.allDestHapPath.push(destFile);
      } else {
        moduleTargetData.getTargetDeviceTypeClasses().forEach((deviceTypeClass) => {
          const outputHapFileName = moduleTargetData.getModuleTargetOutputFileName('', false, undefined, deviceTypeClass);
          const hapPath: string = path.resolve(moduleBuildOutputPath, outputHapFileName);
          const destFile = this.destFileResolve(destDir, outputHapFileName);
          this.allDestDir.push(destDir);
          this.allSrcHapPath.push(hapPath);
          this.allDestHapPath.push(destFile);
        });
      }
    }
  }

  private fastInitWithHapModuleData(moduleTargetData: ModuleTargetData, moduleBuildOutputPath: string) {
    if (!moduleBuildOutputPath) {
      return;
    }
    const destDir = path.resolve(moduleBuildOutputPath, 'app');
    const moduleType = moduleTargetData.getModuleModel().getModuleType();
    const fastAppStorageDir = moduleTargetData.getPathInfo().getFastAppStorageDirectory();
    let relatedEntryModules = moduleTargetData.getRelatedEntryModules();

    this.allSrcHapPath.push(fastAppStorageDir);
    this.allDestDir.push(destDir);

    if (ModuleTaskService.checkEntryModules(moduleType, relatedEntryModules)) {
      if (!this.service.isFaMode()) {
        relatedEntryModules = [relatedEntryModules![0]];
      }
      for (const entryName of relatedEntryModules!) {
        const destFile = this.destFileResolve(destDir, entryName);
        this.allDestHapPath.push(destFile);
      }
    } else {
      const destFile = this.destFileResolve(destDir, moduleTargetData.getModuleModel().getName());
      this.allDestHapPath.push(destFile);
    }
  }

  private initWithHspModuleData(moduleTargetData: ModuleTargetData, moduleBuildOutputPath: string) {
    const outputHspFileName: string = moduleTargetData.getModuleTargetOutputFileName('', false, BuildArtifactExtension.DOT_HSP);
    const hspPath: string = path.resolve(moduleBuildOutputPath, outputHspFileName);
    const destDir = path.resolve(moduleBuildOutputPath, 'app');
    this.allDestDir.push(destDir);
    const destFile = path.resolve(destDir, outputHspFileName.replace('-unsigned', ''));
    this.allSrcHspPath.push(hspPath);
    this.allDestHspPathList.push(destFile);
  }

  private fastInitWithHspModuleData(moduleTargetData: ModuleTargetData, moduleBuildOutputPath: string) {
    if (!moduleBuildOutputPath) {
      return;
    }
    const destDir = path.resolve(moduleBuildOutputPath, 'app');
    const fastAppStorageDir = moduleTargetData.getPathInfo().getFastAppStorageDirectory();
    const destFile = this.destFileResolve(destDir, moduleTargetData.getModuleModel().getName());
    this.allDestDir.push(destDir);
    this.allSrcHspPath.push(fastAppStorageDir);
    this.allDestHspPathList.push(destFile);
  }

  /**
   * 检查打app包时传入包包名是否重复
   * @param paths
   * @private
   */
  private checkOutputName(paths: string[]) {
    if(isFastBuildApp(this.service)) {
      return;
    }
    const outputNameSet = new Set<string>();
    paths.forEach((output) => {
      const outputName = output.slice(output.lastIndexOf(path.sep) + 1);
      if (outputNameSet.has(outputName)) {
        this._log.printErrorExit('DUPLICATE_OUTPUT_NAME', [outputName, output]);
      } else {
        outputNameSet.add(outputName);
      }
    });
  }

  /**
   * 检查是否需要收集debug信息
   */
  private shouldCollectProjectDebugSymbol() {
    return hvigorCore.getParameter().getProperty(CommonConst.USE_COLLECT_DEBUG_SYMBOL) ?? !this.service.getBuildOption()?.debuggable;
  }

  /**
   * 对于已有的hap和hsp模块的debug symbol执行软连接到App scope的build目录下
   * 遍历所有的子模块，然后获得这些子模块的路径，创建这些子模块的路径到project build的目录下
   * 在packApp的daTaskAction里执行，保证依赖任务执行完毕
   * @private
   */
  private collectDebugSymbolInAppScope() {
    // 链接的dest文件夹
    const desProjectDebugSymbolPath = this.getDestinationPath();
    // 保证该文件存在
    fse.ensureDirSync(desProjectDebugSymbolPath);
    // 遍历所有构建出product的modules
    this.service.getProductDataMap().forEach((value) => {
      for (const moduleTargetData of value) {
        const modulePathInfo: ModulePathInfoIml = moduleTargetData.getPathInfo();
        const moduleName: string = moduleTargetData.getModuleModel().getName();
        const moduleBuildOutputPath: string = modulePathInfo.getModuleBuildOutputPath();
        // 每个module所对应的src和已经在project build目录下的dest路径
        const srcModuleDebugSymbolPath = this.getSrcModuleDebugSymbolPath(moduleBuildOutputPath);
        this.createSymbolicLink(srcModuleDebugSymbolPath, desProjectDebugSymbolPath, moduleName);
      }
    });
  }

  /**
   * 返回输出目标路径
   */
  private getDestinationPath(): string {
    return this.service.getBuildOption()?.debuggable
      ? path.resolve(this.pathInfo.getProjectDebugSymbolPath(), BuildModeConst.DEBUG)
      : path.resolve(this.pathInfo.getProjectDebugSymbolPath(), BuildModeConst.RELEASE);
  }
  /**
   * 工程级任务下提取模块下debug symbol路径
   * @return destinationPath
   */
  private getSrcModuleDebugSymbolPath(moduleBuildOutputPath: string): string {
    if (!moduleBuildOutputPath) {
      return '';
    }
    // 检查并创建目标文件夹
    const srcPath = this.service.getBuildOption()?.debuggable
      ? path.resolve(moduleBuildOutputPath, BuildDirConst.OUTPUT_SYMBOL, BuildModeConst.DEBUG)
      : path.resolve(moduleBuildOutputPath, BuildDirConst.OUTPUT_SYMBOL, BuildModeConst.RELEASE);
    if (!fse.existsSync(srcPath)) {
      fse.mkdirSync(srcPath, { recursive: true });
    }
    return srcPath;
  }

  /**
   * 创建软连接，考虑跨平台兼容性
   * @param srcPath
   * @param destPath 该父路径应该保证存在
   * @param moduleName
   * @private
   */
  private createSymbolicLink(srcPath: string, destPath: string, moduleName: string) {
    // 输入保护：如果任一参数为空或未定义，则不执行逻辑
    if (!srcPath || !destPath || !moduleName) {
      return;
    }
    // srcPath校验
    if (!fse.existsSync(srcPath)) {
      return;
    }
    // 执行创建软连接逻辑
    const IS_WINDOWS = process.platform === 'win32' || os.type() === 'Windows_NT';
    const symlinkType = IS_WINDOWS ? 'junction' : 'dir';
    const updatedDestPath = path.resolve(destPath, moduleName);
    // 创建软连接时，应保证父路径存在，以及保证该文件夹不存在
    try {
      if (!fse.existsSync(updatedDestPath) && fse.existsSync(destPath)) {
        fse.symlinkSync(srcPath, updatedDestPath, symlinkType);
      }
    } catch (err) {
      // 创建软连接失败，给出告警，警告包含node原生错误
      this._log.warn(`Create symbolic link for ${srcPath} failed. Reason: ${err}`);
    }
  }

  /**
   * 将目标文件夹打包成zip包
   */
  private async buildZipPackage(): Promise<void> {
    const destinationPath = this.getDestinationPath();
    // 临时目录防止读写冲突
    const tempDir =  path.resolve(destinationPath, '..', 'temp');
    fse.mkdirSync(tempDir, { recursive: true });
    const tempZipOutPath = path.resolve( tempDir, `${BuildArtifactConst.APP_SYMBOL}${BuildArtifactExtension.DOT_ZIP}`);
    const finalZipOutPath = path.resolve(destinationPath, `${BuildArtifactConst.APP_SYMBOL}${BuildArtifactExtension.DOT_ZIP}`);

    const visitedPaths = new Set<string>(); // 记录已处理的文件，防止循环引用
    const zipWriter = new zip.ZipWriter(new zip.BlobWriter('application/zip'));

    // 由于子文件夹和软连接的存在，需要递归打包成zip包
    async  function addFilesToZip(basePath: string, relativePath = ''): Promise<void>{
      const entries = fse.readdirSync(basePath, {withFileTypes: true});
      for (const entry of entries){
        const fullPath = path.join(basePath, entry.name); // 完整路径
        const zipPath = path.posix.join(relativePath, entry.name); // 在 ZIP 文件中的路径
        // 三种情况：文件，文件夹，软连接
        if (entry.isSymbolicLink()){
          const realPath = fse.realpathSync(fullPath);
          if (visitedPaths.has(realPath)) {
            continue; // 防止软连接的循环引用
          }
          visitedPaths.add(realPath);
          // 在此任务中，软连接指向的只会存在文件夹的情况，所以直接对真实路径的文件夹递归处理
          await addFilesToZip(realPath, zipPath);
        } else if (entry.isDirectory()) {
          await addFilesToZip(fullPath, zipPath);
        } else if (entry.isFile()) {
          const fileContent = fse.readFileSync(fullPath);
          await zipWriter.add(zipPath, new zip.Uint8ArrayReader(new Uint8Array(fileContent)));
        }
      }
    }

    try {
      await addFilesToZip(destinationPath);
      const zipBlob = await zipWriter.close();
      const buffer = Buffer.from(await zipBlob.arrayBuffer());
      // 写入临时 ZIP 文件
      fse.writeFileSync(tempZipOutPath, buffer); // 写入临时 ZIP 文件
      // 将临时文件移动到最终目录
      fse.renameSync(tempZipOutPath, finalZipOutPath);
    } catch (err) {
      // 打包失败了之后，给出warn告警，警告包含node原生错误
      this._log.warn(`Packing app-symbol.zip failed. Reason: ${err}`);
    } finally {
      // 删除临时目录
      fse.removeSync(tempDir);
    }
  }

  private destFileResolve(dir:string, outPath:string) {
    return path.resolve(dir, outPath.replace('-unsigned', ''));
  }
}
